/*
 *    Copyright 2022 The DSMS Authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.dsms.modules.storagepool.task;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ceph.rados.IoCTX;
import com.ceph.rados.Rados;
import com.dsms.common.constant.*;
import com.dsms.common.exception.DsmsEngineException;
import com.dsms.common.remotecall.model.FailedDetail;
import com.dsms.common.remotecall.model.FinishedDetail;
import com.dsms.common.remotecall.model.RemoteResponse;
import com.dsms.common.taskmanager.TaskException;
import com.dsms.common.taskmanager.TaskStrategy;
import com.dsms.common.taskmanager.model.Step;
import com.dsms.common.taskmanager.model.Task;
import com.dsms.common.taskmanager.service.IStepService;
import com.dsms.common.taskmanager.service.ITaskService;
import com.dsms.dfsbroker.osd.crushmap.api.CrushmapApi;
import com.dsms.dfsbroker.osd.crushmap.request.CreateBucketRequest;
import com.dsms.dfsbroker.osd.crushmap.request.DelBucketRequest;
import com.dsms.dfsbroker.osd.crushmap.request.DelCrushRuleRequest;
import com.dsms.dfsbroker.osd.ecprofile.api.EcProfileApi;
import com.dsms.dfsbroker.osd.ecprofile.model.dto.EcProfileDto;
import com.dsms.dfsbroker.osd.ecprofile.request.DeleteEcProfileRequest;
import com.dsms.dfsbroker.storagepool.api.StoragePoolApi;
import com.dsms.dfsbroker.storagepool.model.dto.ErasureCreateDTO;
import com.dsms.dfsbroker.storagepool.model.dto.ReplicatedCreateDTO;
import com.dsms.dfsbroker.storagepool.model.dto.StoragePoolCreateDTO;
import com.dsms.dfsbroker.storagepool.request.AllowEcOverWrites;
import com.dsms.dfsbroker.storagepool.request.CreateStoragePoolRequest;
import com.dsms.modules.util.RadosSingleton;
import com.dsms.modules.util.RemoteCallUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

@Slf4j
@Service(TaskTypeEnum.TypeConstants.CREATE_POOL)
public class CreateStoragePoolTask implements TaskStrategy {
    @Autowired
    private IStepService stepService;
    @Autowired
    private CrushmapApi crushmapApi;
    @Autowired
    private EcProfileApi ecProfileApi;
    @Autowired
    private StoragePoolApi storagePoolApi;
    @Autowired
    private ITaskService taskService;

    @Override
    public Task execute(Task task) {
        String taskParam = task.getTaskParam();
        if (ObjectUtils.isEmpty(taskParam)) {
            task.setTaskEndTime(LocalDateTime.now());
            task.setTaskStatus(TaskStatusEnum.FAIL.getStatus());
            task.setTaskErrorMessage("task parameter is null");
            return task;
        }

        //1.get step task
        Integer taskId = task.getId();
        List<Step> steps = stepService.list(new LambdaQueryWrapper<Step>().eq(Step::getTaskId, taskId));
        String taskMessage = task.getTaskMessage();

        //2.create root bucket
        StoragePoolCreateDTO storagePoolCreateDTO = JSONUtil.toBean(taskParam, StoragePoolCreateDTO.class);
        Step createRootBucketStep = createRootBucket(storagePoolCreateDTO.getPoolName(),
                steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.CREATE_ROOT_BUCKET.getType())).findFirst().orElse(new Step()));

        //create root bucket failed
        if (!Objects.equals(createRootBucketStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
            task.setTaskErrorMessage(createRootBucketStep.getStepErrorMessage());
            task.setTaskStatus(TaskStatusEnum.FAIL.getStatus());
            return task;
        }

        //3.create storage pool according to pool type
        if (taskMessage.contains(StoragePoolTypeEnum.REPLICATED.getType())) {
            //replicated pool
            ReplicatedCreateDTO replicatedCreateDTO = JSONUtil.toBean(taskParam, ReplicatedCreateDTO.class);

            //3.1 create rule
            Step createRuleStep = createRule(replicatedCreateDTO.getPoolName(),
                    steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.CREATE_RULE.getType())).findFirst().orElse(new Step()));

            if (Objects.equals(createRuleStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                //3.2 create pool
                Step createStoragePoolStep = createStoragePool(replicatedCreateDTO,
                        steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.CREATE_REPLICATED_POOL.getType())).findFirst().orElse(new Step()));
                if (Objects.equals(createStoragePoolStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                    task.setTaskStatus(TaskStatusEnum.FINISH.getStatus());
                    return task;
                } else {
                    task.setTaskErrorMessage(createStoragePoolStep.getStepErrorMessage());
                }
            } else {
                task.setTaskErrorMessage(createRuleStep.getStepErrorMessage());
            }
        } else if (taskMessage.contains(StoragePoolTypeEnum.ERASURE.getType())) {
            //erasure pool
            ErasureCreateDTO erasureCreateDTO = JSONUtil.toBean(taskParam, ErasureCreateDTO.class);

            //3.1 create erasure pool
            Step createEcProfileStep = createEcProfile(erasureCreateDTO.getEcProfileDto(),
                    steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.CREATE_ECPROFILE.getType())).findFirst().orElse(new Step()));

            if (Objects.equals(createEcProfileStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                //3.2 create erasure pool
                Step createStoragePoolStep = createStoragePool(erasureCreateDTO,
                        steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.CREATE_ERASURE_POOL.getType())).findFirst().orElse(new Step()));
                if (Objects.equals(createStoragePoolStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                    //3.3 The erasure code pool needs to enable the feature allowed to create rbd and filesystem
                    Step allowEcOverWritesStep = allowEcOverWritesStep(erasureCreateDTO.getPoolName(),
                            steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.ALLOW_EC_OVER_WRITES.getType())).findFirst().orElse(new Step()));
                    if (Objects.equals(allowEcOverWritesStep.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                        task.setTaskStatus(TaskStatusEnum.FINISH.getStatus());
                        return task;
                    } else {
                        task.setTaskErrorMessage(allowEcOverWritesStep.getStepErrorMessage());
                    }
                } else {
                    task.setTaskErrorMessage(createStoragePoolStep.getStepErrorMessage());
                }
            } else {
                task.setTaskErrorMessage(createEcProfileStep.getStepErrorMessage());
            }
        }

        //update task
        task.setTaskStatus(TaskStatusEnum.ROLLBACK.getStatus());
        return task;
    }

    private Step createStoragePool(StoragePoolCreateDTO storagePoolCreateDTO, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse createResponse = storagePoolApi.createPool(RemoteCallUtil.generateRemoteRequest(), storagePoolCreateDTO);
            boolean flag = true;//use to check whether the task in cluster is complete
            while (flag) {
                getStepResponse(step, createResponse, String.format(CreateStoragePoolRequest.CREATE_POOL_SUCCESS, storagePoolCreateDTO.getPoolName()), "create pool fail");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }
        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
        }
        return step;
    }

    private Step createEcProfile(EcProfileDto ecProfileDto, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse createResponse = ecProfileApi.createEcProfile(RemoteCallUtil.generateRemoteRequest(), ecProfileDto);
            boolean flag = true;//use to check whether the task in cluster is complete
            while (flag) {
                getStepResponse(step, createResponse, "", "create ec-profile fail");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }

        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
        }
        return step;
    }

    private Step createRule(String ruleName, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse createResponse = crushmapApi.createRule(RemoteCallUtil.generateRemoteRequest(), ruleName);
            boolean flag = true;//use to check whether the task in cluster is complete
            while (flag) {
                getStepResponse(step, createResponse, "", "create rule fail");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }
        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
        }
        return step;
    }

    private Step createRootBucket(String bucketName, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse createResponse = crushmapApi.createBucket(RemoteCallUtil.generateRemoteRequest(), bucketName, CrushFailureDomainEnum.ROOT.getTypeId(), "");
            boolean flag = true;//use to check whether the task in cluster is complete
            while (flag) {
                getStepResponse(step, createResponse, String.format(CreateBucketRequest.CREATE_ROOT_BUCKET_SUCCESS, bucketName), "create root bucket result fail");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }
        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
        }
        return step;
    }

    private Step allowEcOverWritesStep(String erasurePoolName, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        Rados rados = RadosSingleton.INSTANCE.getRados();
        try (IoCTX ioctx = rados.ioCtxCreate(erasurePoolName)) {
            int poolId = (int) ioctx.getId();
            RemoteResponse allowResponse = storagePoolApi.allowEcOverWrites(RemoteCallUtil.generateRemoteRequest(), erasurePoolName);
            boolean flag = true;//use to check whether the task in cluster is complete
            while (flag) {
                getStepResponse(step, allowResponse, String.format(AllowEcOverWrites.ALLOW_EC_OVER_WRITES_SUCCESS, poolId), "allow ec over writes fail");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }

        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
        }
        return step;
    }

    private Step getStepResponse(Step step, RemoteResponse executeResponse, String require, String errMsg) throws Throwable {
        //get response of step request
        RemoteResponse resultResponse = crushmapApi.getUpdateCrushmapResult(RemoteCallUtil.generateRemoteRequest(), executeResponse.getId());

        //analytic response result
        if (Objects.equals(resultResponse.getState(), RemoteResponseStatusEnum.SUCCESS.getMessage())) {
            List<FinishedDetail> finished = resultResponse.getFinished();
            String outs = finished.get(0).getOuts();
            if (Objects.equals(require, outs)) {
                step.setStepStatus(TaskStatusEnum.FINISH.getStatus());
            } else {
                step.setStepErrorMessage(outs);
                step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
                stepService.updateById(step);
                throw new TaskException(errMsg + outs);
            }
        } else if (Objects.equals(resultResponse.getState(), RemoteResponseStatusEnum.FAILED.getMessage())) {
            List<FailedDetail> failed = resultResponse.getFailed();
            String outs = failed.get(0).getOuts();
            step.setStepErrorMessage(outs);
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
            stepService.updateById(step);
            throw new TaskException(errMsg + "," + outs);
        } else if (ObjectUtils.isEmpty(resultResponse.getState())) {
            stepService.updateById(step);
            throw new TaskException("unknown request state,response:" + JSON.toJSONString(resultResponse));
        }
        stepService.updateById(step);
        return step;
    }


    @Override
    public boolean validateTask(String[] validateParam) {
        return taskService.validateTaskMessageAndTaskType(validateParam[0]);
    }

    @Override
    public Task rollback(Task task) {
        String taskParam = task.getTaskParam();
        if (ObjectUtils.isEmpty(taskParam)) {
            task.setTaskEndTime(LocalDateTime.now());
            task.setTaskStatus(TaskStatusEnum.FAIL.getStatus());
            task.setTaskErrorMessage("task parameter is null");
            return task;
        }
        StoragePoolCreateDTO storagePoolCreateDTO = JSONUtil.toBean(taskParam, StoragePoolCreateDTO.class);
        String poolName = storagePoolCreateDTO.getPoolName();
        //only if these additional resources removed successfully,this task turned to ROLLBACK_SUCCESSFUL status, or it will be ROLLBACK status forever
        try {
            //1.delete rule if exist
            RemoteResponse delRuleResponse = crushmapApi.delCrushRule(RemoteCallUtil.generateRemoteRequest(), poolName);
            String delRuleOut = getResponseOut(delRuleResponse);
            //get delete rule result error
            if (delRuleOut == null) {
                return task;
            }
            //rule exists but delete failed
            if (!(Objects.equals(String.format(DelCrushRuleRequest.DOES_NOT_EXISTS, poolName), delRuleOut) || CharSequenceUtil.isEmpty(delRuleOut))) {
                return task;
            }

            //2.delete bucket if exist
            RemoteResponse delBucketResponse = crushmapApi.delBucket(RemoteCallUtil.generateRemoteRequest(), poolName);
            String delBucketOut = getResponseOut(delBucketResponse);
            if (delBucketOut == null) {
                return task;
            }
            //bucket exists but delete failed
            if (!(Objects.equals(String.format(DelBucketRequest.DOES_NOT_APPEAR, poolName), delBucketOut)
                    || delBucketOut.startsWith(DelBucketRequest.DELETE_BUCKET_SUCCESS))) {
                return task;
            }
            //3.delete ec-profile if it is erasure storage pool
            if (Objects.equals(storagePoolCreateDTO.getPoolType(), StoragePoolTypeEnum.ERASURE.getCode())) {
                RemoteResponse deleteEcProfileResponse = ecProfileApi.deleteEcProfile(RemoteCallUtil.generateRemoteRequest(), poolName);
                String deleteEcProfileOut = getResponseOut(deleteEcProfileResponse);
                //get delete ec-profile result error
                if (deleteEcProfileOut == null) {
                    return task;
                }
                //ec-profile exists but delete failed
                if (!(Objects.equals(String.format(DeleteEcProfileRequest.DOES_NOT_EXIST, poolName), deleteEcProfileOut) || CharSequenceUtil.isEmpty(delRuleOut))) {
                    return task;
                }
            }
        } catch (Throwable e) {
            log.error("create pool failed and roll back failed,try roll back later:{}", e.getMessage(), e);
            throw new RuntimeException(e);
        }
        task.setTaskStatus(TaskStatusEnum.ROLLBACK_SUCCESS.getStatus());
        task.setTaskEndTime(LocalDateTime.now());
        return task;
    }

    /**
     * parse the out of response
     *
     * @param response ceph's response of task
     * @return out if it has,null if error
     */
    private String getResponseOut(RemoteResponse response) {
        String out = null;
        String outs;
        String outb;
        if (Objects.equals(response.getState(), RemoteResponseStatusEnum.SUCCESS.getMessage())) {
            List<FinishedDetail> finished = response.getFinished();
            outs = finished.get(0).getOuts();
            outb = finished.get(0).getOutb();
            out = StringUtils.hasText(outs) ? outs : outb;
        } else if (Objects.equals(response.getState(), RemoteResponseStatusEnum.FAILED.getMessage())) {
            List<FailedDetail> failed = response.getFailed();
            outs = failed.get(0).getOuts();
            outb = failed.get(0).getOutb();
            out = StringUtils.hasText(outs) ? outs : outb;
        }
        return out;
    }
}